<template>
  <div id="app" class="container">
    <div class="header">
      <h1>股票K线图</h1>
      <p>支持缩放、拖动查看，包含成交量和技术指标</p>
    </div>

    <div class="chart-container">
      <div class="chart-header">
        <div class="stock-info">
          <div class="stock-name">示例股票 (AAPL)</div>
          <div class="price" :class="currentPrice >= stockData[0]?.收盘价 ? 'up' : 'down'">
            {{ currentPrice.toFixed(2) }}
          </div>
          <div class="change" :class="priceChange >= 0 ? 'up' : 'down'">
            {{ priceChange >= 0 ? '+' : '' }}{{ priceChange.toFixed(2) }} ({{ priceChangePercent.toFixed(2) }}%)
          </div>
        </div>
        <div class="controls">
          <button @click="zoomIn">放大</button>
          <button @click="zoomOut">缩小</button>
          <button @click="resetZoom">重置</button>
        </div>
      </div>

      <div class="chart-wrapper" ref="chartWrapper">
        <canvas ref="canvas" @mousedown="startDrag" @mousemove="onDrag" @mouseup="endDrag" @mouseleave="endDrag"
                @touchstart="startDrag" @touchmove="onDrag" @touchend="endDrag" @wheel="onWheel"></canvas>
        <div class="tooltip" v-if="tooltip.visible" :style="{ left: tooltip.x + 'px', top: tooltip.y + 'px' }">
          <div class="tooltip-line">
            <span class="tooltip-label">日期:</span>
            <span>{{ tooltip.data.日期 }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">开盘:</span>
            <span>{{ tooltip.data.开盘价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">最高:</span>
            <span>{{ tooltip.data.最高价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">最低:</span>
            <span>{{ tooltip.data.最低价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">收盘:</span>
            <span>{{ tooltip.data.收盘价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">成交量:</span>
            <span>{{ formatVolume(tooltip.data.成交量) }}</span>
          </div>
        </div>
      </div>

      <div class="legend">
        <div class="legend-item">
          <div class="legend-color" style="background-color: #00b894;"></div>
          <span>阳线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #ff7675;"></div>
          <span>阴线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #74b9ff;"></div>
          <span>5日均线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #a29bfe;"></div>
          <span>20日均线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #ffeaa7;"></div>
          <span>40日均线</span>
        </div>
      </div>
    </div>

    <div class="time-frame-selector">
      <button class="time-frame-btn" :class="{ active: timeFrame === '1D' }" @click="setTimeFrame('1D')">1日</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '1W' }" @click="setTimeFrame('1W')">1周</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '1M' }" @click="setTimeFrame('1M')">1月</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '3M' }" @click="setTimeFrame('3M')">3月</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '1Y' }" @click="setTimeFrame('1Y')">1年</button>
    </div>

    <div class="instructions">
      <h3>操作指南:</h3>
      <ul>
        <li>鼠标滚轮或双指捏合: 缩放图表</li>
        <li>鼠标拖动或单指滑动: 平移图表</li>
        <li>点击上方按钮: 切换时间周期</li>
        <li>鼠标悬停在K线上: 查看详细数据</li>
      </ul>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      stockData: [],
      currentPrice: 0,
      priceChange: 0,
      priceChangePercent: 0,
      timeFrame: '1M',
      tooltip: {
        visible: false,
        x: 0,
        y: 0,
        data: {}
      },
      // 图表配置
      config: {
        candleWidth: 10,
        gap: 2,
        padding: {
          top: 20,
          right: 60,
          bottom: 80,
          left: 60
        },
        colors: {
          up: '#00b894',
          down: '#ff7675',
          volume: '#636e72',
          ma5: '#74b9ff',
          ma20: '#a29bfe',
          ma40: '#ffeaa7',
          grid: '#444',
          text: '#e0e0e0'
        }
      },
      // 视图状态
      viewState: {
        offset: 0,
        scale: 1,
        isDragging: false,
        lastX: 0
      },
      canvas: null,
      ctx: null,
      canvasWidth: 0,
      canvasHeight: 0
    }
  },
  mounted() {
    this.initCanvas();
    this.generateSampleData();
    this.drawChart();
    window.addEventListener('resize', this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize);
  },

  methods: {
    initCanvas() {
      this.canvas = this.$refs.canvas;
      this.ctx = this.canvas.getContext('2d');
      this.handleResize();
    },
    handleResize() {
      const wrapper = this.$refs.chartWrapper;
      this.canvasWidth = wrapper.clientWidth;
      this.canvasHeight = wrapper.clientHeight;
      this.canvas.width = this.canvasWidth;
      this.canvas.height = this.canvasHeight;
      this.drawChart();
    },
    generateSampleData() {
      // 生成模拟数据
      const startDate = new Date('2020-01-01');
      const data = [];
      let price = 100;

      for (let i = 0; i < 500; i++) {
        const date = new Date(startDate);
        date.setDate(date.getDate() + i);

        const open = price;
        const change = (Math.random() - 0.5) * 10;
        const close = open + change;
        const high = Math.max(open, close) + Math.random() * 5;
        const low = Math.min(open, close) - Math.random() * 5;
        const volume = Math.floor(Math.random() * 1000000) + 500000;

        // 计算移动平均线（简化计算）
        const ma5 = i >= 4 ? data.slice(i-4, i+1).reduce((sum, item) => sum + item.收盘价, 0) / 5 : open;
        const ma20 = i >= 19 ? data.slice(i-19, i+1).reduce((sum, item) => sum + item.收盘价, 0) / 20 : open;
        const ma40 = i >= 39 ? data.slice(i-39, i+1).reduce((sum, item) => sum + item.收盘价, 0) / 40 : open;

        data.push({
          日期: date.toISOString().split('T')[0],
          开盘价: open,
          最高价: high,
          最低价: low,
          收盘价: close,
          成交量: volume,
          '5均价': ma5,
          '20均价': ma20,
          '40均价': ma40
        });
        price = close;
      }

      this.stockData = data;
      this.currentPrice = data[data.length - 1].收盘价;
      this.priceChange = this.currentPrice - data[0].收盘价;
      this.priceChangePercent = (this.priceChange / data[0].收盘价) * 100;
    },
    drawChart() {
      if (!this.ctx || this.stockData.length === 0) return;

      // 清除画布
      this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);

      // 绘制网格和坐标
      this.drawGrid();

      // 计算可见数据范围
      const visibleData = this.getVisibleData();
      if (visibleData.length === 0) return;

      // 计算价格范围
      const priceRange = this.calculatePriceRange(visibleData);

      // 绘制K线
      this.drawCandles(visibleData, priceRange);

      // 绘制移动平均线
      this.drawMALines(visibleData, priceRange);

      // 绘制成交量
      this.drawVolume(visibleData);
    },
    drawGrid() {
      const { ctx, canvasWidth, canvasHeight, config } = this;
      const { padding, colors } = config;

      ctx.strokeStyle = colors.grid;
      ctx.lineWidth = 0.5;
      ctx.fillStyle = colors.text;
      ctx.font = '12px Arial';

      // 水平网格线
      const horizontalLines = 5;
      for (let i = 0; i <= horizontalLines; i++) {
        const y = padding.top + (canvasHeight - padding.top - padding.bottom) * (i / horizontalLines);
        ctx.beginPath();
        ctx.moveTo(padding.left, y);
        ctx.lineTo(canvasWidth - padding.right, y);
        ctx.stroke();

        // 价格标签
        const price = this.getPriceForY(y);
        ctx.textAlign = 'right';
        ctx.fillText(price.toFixed(2), padding.left - 5, y + 4);
      }

      // 垂直网格线（时间轴）
      const visibleData = this.getVisibleData();
      if (visibleData.length > 0) {
        const step = Math.max(1, Math.floor(visibleData.length / 5));
        for (let i = 0; i < visibleData.length; i += step) {
          const x = this.getXForIndex(i);
          ctx.beginPath();
          ctx.moveTo(x, padding.top);
          ctx.lineTo(x, canvasHeight - padding.bottom);
          ctx.stroke();

          // 日期标签
          ctx.textAlign = 'center';
          ctx.fillText(visibleData[i].日期, x, canvasHeight - padding.bottom + 20);
        }
      }

      // 绘制Y轴标签（右侧）
      ctx.textAlign = 'left';
      for (let i = 0; i <= horizontalLines; i++) {
        const y = padding.top + (canvasHeight - padding.top - padding.bottom) * (i / horizontalLines);
        const price = this.getPriceForY(y);
        ctx.fillText(price.toFixed(2), canvasWidth - padding.right + 5, y + 4);
      }
    },
    drawCandles(visibleData, priceRange) {
      const { ctx, config } = this;
      const { padding, colors } = config;
      const chartHeight = this.canvasHeight - padding.top - padding.bottom;

      for (let i = 0; i < visibleData.length; i++) {
        const data = visibleData[i];
        const x = this.getXForIndex(i);
        const isUp = data.收盘价 >= data.开盘价;

        // 计算K线的Y坐标和高度
        const highY = this.getYForPrice(data.最高价, priceRange);
        const lowY = this.getYForPrice(data.最低价, priceRange);
        const openY = this.getYForPrice(data.开盘价, priceRange);
        const closeY = this.getYForPrice(data.收盘价, priceRange);

        // 绘制上下影线
        ctx.strokeStyle = isUp ? colors.up : colors.down;
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(x, highY);
        ctx.lineTo(x, lowY);
        ctx.stroke();

        // 绘制实体
        const bodyTop = Math.min(openY, closeY);
        const bodyHeight = Math.abs(openY - closeY);
        const bodyWidth = Math.max(1, this.config.candleWidth * this.viewState.scale);

        ctx.fillStyle = isUp ? colors.up : colors.down;
        ctx.fillRect(x - bodyWidth/2, bodyTop, bodyWidth, bodyHeight);
      }
    },
    drawMALines(visibleData, priceRange) {
      const { ctx, config } = this;
      const { colors } = config;

      // 绘制5日均线
      this.drawMALine(visibleData, priceRange, '5均价', colors.ma5);

      // 绘制20日均线
      this.drawMALine(visibleData, priceRange, '20均价', colors.ma20);

      // 绘制40日均线
      this.drawMALine(visibleData, priceRange, '40均价', colors.ma40);
    },
/*    drawMALine(visibleData, priceRange, maKey, color) {
      const { ctx } = this;

      ctx.strokeStyle = color;
      ctx.lineWidth = 1.5;
      ctx.beginPath();

      for (let i = 0; i < visibleData.length; i++) {
        const data = visibleData[i];
        if (data[maKey] === undefined) continue;

        const x = this.getXForIndex(i);
        const y = this.getYForPrice(data[maKey], priceRange);

        if (i === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      }

      ctx.stroke();
    },*/
    drawVolume(visibleData) {
      const { ctx, canvasHeight, config } = this;
      const { padding, colors } = config;

      const volumeHeight = 100;
      const volumeTop = canvasHeight - padding.bottom - volumeHeight;

      // 计算成交量范围
      const volumeRange = this.calculateVolumeRange(visibleData);

      // 绘制成交量柱
      for (let i = 0; i < visibleData.length; i++) {
        const data = visibleData[i];
        const x = this.getXForIndex(i);
        const isUp = data.收盘价 >= data.开盘价;
        const volumeY = this.getYForVolume(data.成交量, volumeRange, volumeHeight);
        const volumeWidth = Math.max(1, this.config.candleWidth * this.viewState.scale * 0.8);

        ctx.fillStyle = isUp ? colors.up : colors.down;
        ctx.fillRect(x - volumeWidth/2, volumeY, volumeWidth, volumeTop + volumeHeight - volumeY);
      }

      // 绘制成交量区域边框
      ctx.strokeStyle = colors.grid;
      ctx.lineWidth = 0.5;
      ctx.strokeRect(padding.left, volumeTop, this.canvasWidth - padding.left - padding.right, volumeHeight);
    },
    drawMALine(visibleData, priceRange, maKey, color) {
      const { ctx } = this;

      ctx.strokeStyle = color;
      ctx.lineWidth = 1.5;
      ctx.beginPath();

      let started = false;
      for (let i = 0; i < visibleData.length; i++) {
        const data = visibleData[i];
        if (data[maKey] === undefined) continue;

        const x = this.getXForIndex(i);
        const y = this.getYForPrice(data[maKey], priceRange);

        if (!started) {
          ctx.moveTo(x, y);
          started = true;
        } else {
          ctx.lineTo(x, y);
        }
      }

      ctx.stroke();
    },
    getVisibleData() {
      const { offset, scale } = this.viewState;
      const start = Math.max(0, Math.floor(offset));
      const end = Math.min(this.stockData.length, Math.ceil(offset + this.stockData.length / scale));
      return this.stockData.slice(start, end);
    },
    calculatePriceRange(visibleData) {
      let min = Number.MAX_VALUE;
      let max = Number.MIN_VALUE;

      for (const data of visibleData) {
        min = Math.min(min, data.最低价);
        max = Math.max(max, data.最高价);
      }

      // 添加一些边距
      const margin = (max - min) * 0.05;
      return {
        min: min - margin,
        max: max + margin
      };
    },
    calculateVolumeRange(visibleData) {
      let max = 0;

      for (const data of visibleData) {
        max = Math.max(max, data.成交量);
      }

      return {
        min: 0,
        max: max
      };
    },
    getXForIndex(index) {
      const { padding } = this.config;
      const visibleData = this.getVisibleData();
      const chartWidth = this.canvasWidth - padding.left - padding.right;

      if (visibleData.length <= 1) {
        return padding.left + chartWidth / 2;
      }

      const relativeIndex = index - this.viewState.offset;
      const totalVisible = this.stockData.length / this.viewState.scale;
      return padding.left + (relativeIndex / totalVisible) * chartWidth;
    },
    getYForPrice(price, priceRange) {
      const { padding } = this.config;
      const chartHeight = this.canvasHeight - padding.top - padding.bottom;
      return padding.top + chartHeight * (1 - (price - priceRange.min) / (priceRange.max - priceRange.min));
    },
    getYForVolume(volume, volumeRange, volumeHeight) {
      const { padding } = this.config;
      const volumeTop = this.canvasHeight - padding.bottom - volumeHeight;
      return volumeTop + volumeHeight * (1 - volume / volumeRange.max);
    },
    getPriceForY(y) {
      const { padding } = this.config;
      const visibleData = this.getVisibleData();
      const priceRange = this.calculatePriceRange(visibleData);
      const chartHeight = this.canvasHeight - padding.top - padding.bottom;
      return priceRange.max - (y - padding.top) / chartHeight * (priceRange.max - priceRange.min);
    },
    startDrag(event) {
      event.preventDefault();
      this.viewState.isDragging = true;
      this.viewState.lastX = this.getEventX(event);
    },
    onDrag(event) {
      if (!this.viewState.isDragging) return;
      event.preventDefault();

      const currentX = this.getEventX(event);
      const deltaX = currentX - this.viewState.lastX;

      // 计算移动的索引数量
      const visibleCount = this.stockData.length / this.viewState.scale;
      const chartWidth = this.canvasWidth - this.config.padding.left - this.config.padding.right;
      const deltaIndex = (deltaX / chartWidth) * visibleCount;

      this.viewState.offset = Math.max(0, Math.min(this.stockData.length - visibleCount, this.viewState.offset - deltaIndex));
      this.viewState.lastX = currentX;

      this.drawChart();
      this.updateTooltip(event);
    },
    endDrag() {
      this.viewState.isDragging = false;
    },
    onWheel(event) {
      event.preventDefault();

      const delta = Math.sign(event.deltaY);
      const zoomFactor = delta > 0 ? 1.1 : 0.9;

      // 计算缩放中心
      const mouseX = this.getEventX(event);
      const visibleCount = this.stockData.length / this.viewState.scale;
      const chartWidth = this.canvasWidth - this.config.padding.left - this.config.padding.right;
      const mouseIndex = (mouseX - this.config.padding.left) / chartWidth * visibleCount + this.viewState.offset;

      // 应用缩放
      const newScale = Math.max(1, Math.min(20, this.viewState.scale * zoomFactor));

      // 调整偏移量以保持鼠标位置
      const newVisibleCount = this.stockData.length / newScale;
      const newOffset = Math.max(0, Math.min(this.stockData.length - newVisibleCount,
        mouseIndex - (mouseX - this.config.padding.left) / chartWidth * newVisibleCount));

      this.viewState.scale = newScale;
      this.viewState.offset = newOffset;

      this.drawChart();
    },
    getEventX(event) {
      if (event.type.includes('touch')) {
        return event.touches[0].clientX - this.canvas.getBoundingClientRect().left;
      } else {
        return event.offsetX;
      }
    },
    updateTooltip(event) {
      const x = this.getEventX(event);
      const visibleData = this.getVisibleData();

      if (visibleData.length === 0) {
        this.tooltip.visible = false;
        return;
      }

      // 找到最接近的K线
      const chartWidth = this.canvasWidth - this.config.padding.left - this.config.padding.right;
      const index = Math.floor((x - this.config.padding.left) / chartWidth * visibleData.length);

      if (index >= 0 && index < visibleData.length) {
        const data = visibleData[index];
        this.tooltip.visible = true;
        this.tooltip.x = x + 10;
        this.tooltip.y = 10;
        this.tooltip.data = data;
      } else {
        this.tooltip.visible = false;
      }
    },
    zoomIn() {
      this.viewState.scale = Math.min(20, this.viewState.scale * 1.2);
      this.drawChart();
    },
    zoomOut() {
      this.viewState.scale = Math.max(1, this.viewState.scale / 1.2);
      this.drawChart();
    },
    resetZoom() {
      this.viewState.scale = 1;
      this.viewState.offset = 0;
      this.drawChart();
    },
    setTimeFrame(timeFrame) {
      this.timeFrame = timeFrame;
      // 在实际应用中，这里应该根据选择的时间范围加载相应的数据
      // 这里我们只是重新生成一些数据来模拟
      this.generateSampleData();
      this.resetZoom();
    },
    formatVolume(volume) {
      if (volume >= 1000000) {
        return (volume / 1000000).toFixed(2) + 'M';
      } else if (volume >= 1000) {
        return (volume / 1000).toFixed(2) + 'K';
      }
      return volume.toString();
    }
  }
}
</script>
<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #1e1e2e;
  color: #e0e0e0;
  padding: 20px;
}
.container {
  max-width: 1200px;
  margin: 0 auto;
}
.header {
  text-align: center;
  margin-bottom: 20px;
}
.header h1 {
  color: #6c5ce7;
  margin-bottom: 10px;
}
.chart-container {
  background-color: #2d2d44;
  border-radius: 10px;
  padding: 20px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  margin-bottom: 20px;
}
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.stock-info {
  display: flex;
  gap: 20px;
}
.price {
  font-size: 1.2em;
  font-weight: bold;
}
.price.up {
  color: #00b894;
}
.price.down {
  color: #ff7675;
}
.controls {
  display: flex;
  gap: 10px;
}
button {
  background-color: #6c5ce7;
  color: white;
  border: none;
  padding: 8px 15px;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s;
}
button:hover {
  background-color: #5b4bc4;
}
.chart-wrapper {
  position: relative;
  width: 100%;
  height: 500px;
  border: 1px solid #444;
  border-radius: 5px;
  overflow: hidden;
}
canvas {
  display: block;
  background-color: #1e1e2e;
}
.legend {
  display: flex;
  gap: 15px;
  margin-top: 10px;
  font-size: 0.9em;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 5px;
}
.legend-color {
  width: 15px;
  height: 3px;
}
.tooltip {
  position: absolute;
  background-color: rgba(45, 45, 68, 0.9);
  border: 1px solid #555;
  border-radius: 5px;
  padding: 10px;
  pointer-events: none;
  z-index: 100;
  font-size: 0.9em;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.tooltip-line {
  display: flex;
  justify-content: space-between;
  margin-bottom: 5px;
}
.tooltip-line:last-child {
  margin-bottom: 0;
}
.tooltip-label {
  color: #aaa;
  margin-right: 10px;
}
.time-frame-selector {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 20px;
}
.time-frame-btn {
  background-color: #3d3d5c;
}
.time-frame-btn.active {
  background-color: #6c5ce7;
}
.instructions {
  margin-top: 20px;
  padding: 15px;
  background-color: #2d2d44;
  border-radius: 5px;
  font-size: 0.9em;
  line-height: 1.5;
}
</style>